[cherry-pick] 마이페이지 기능 구현 및 디자인 개선#693
Conversation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… API 훅 및 Zod 스키마 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…작성한 글 / 1:1 문의 페이지 구현 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ocalStorage 기반 클라이언트 상태 관리 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
primary-500 미존재 토큰을 fill-brand-default-default 등으로 교체, GiftEmailCard 제거, disabled 버튼 스타일 수정 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
디스코드/피드 배너 레이아웃 개선, primary-500 미존재 토큰을 text-text-brand/border-border-brand으로 교체, 사이드바 라벨 변경 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… 토큰 교체 bg/text/border-primary-500, bg-brand-primary-500을 프로젝트 토큰 (fill-brand-default-default, text-brand, border-brand, rose-500 등)으로 일괄 교체 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
discord-icon.png, feed-icon.svg 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…취소 관련 타입 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… 및 환불 요청 훅 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… 플로우 분리 및 가상계좌 상세 조회 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…UI 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…답 타입 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ation 및 에러 처리 개선 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…lderFeedItemResponse 미사용 타입 제거 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…raftBuilderFeeds 중복 훅 제거
useGetMyBuilderFeedManagement({ status: 'DRAFT' })로 통합
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- feed/write: draftFeedIdRef로 feedId 추적, 재저장 시 updateFeed 사용
- feed/write: 등록하기 시 기존 draft는 updateFeed(PUBLISHED)로 전환
- feed/write: edit mode 수정하기에 status: PUBLISHED 추가
- qa/write: 등록 성공 시 localStorage draft 정리 추가
- my-posts: useGetMyBuilderFeedManagement({ status: DRAFT })로 교체
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… 상세 페이지로 이동 임시저장 글 등록 시 피드 목록이 아닌 발행된 피드 상세로 이동. 신규 등록도 createFeed 응답의 feedId로 상세 페이지 이동. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…E 스킵으로 CI 보호 test.zeroone.it.kr DNS 불통 상태에서 전체 E2E 테스트가 net::ERR_NAME_NOT_RESOLVED로 실패하는 문제 해결. 커넥티비티 프리플라이트 추가 — 응답 없으면 STAGING_DOWN=true 설정 후 E2E 스텝 전체 스킵, 경고 메시지 출력. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…로컬 서버 대상으로 실행해 스테이징 의존성 제거 이전 bandaid fix(스테이징 다운 시 전체 스킵)를 대체. - CI에서 Next.js 앱 직접 빌드 후 localhost:3000 서버 기동 - non-@auth 테스트: 항상 로컬 서버 대상 실행 (스테이징 불필요) - @auth 테스트: 스테이징 가용 시에만 실행, 불가 시 경고 후 스킵 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthrough마이페이지의 1:1 문의/결제/포스트 관리를 API v6으로 전환하고, Builder Feed와 QnA에 localStorage 기반 임시저장 기능을 추가했습니다. 결제 환불·취소 모달과 알림 토글을 개선하고, CI Playwright E2E를 로컬 기반과 스테이징 조건으로 분리했으며 여러 UI 스타일 토큰을 통합했습니다. Changes마이페이지 기능 확장 및 스타일 통합
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 9
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/app/(service)/(my)/my-inquiry/write/page.tsx (1)
157-168:⚠️ Potential issue | 🟡 Minor | ⚡ Quick win체크박스 색상에 하드코딩 Tailwind 색상(
accent-rose-500)을 사용하고 있습니다.앱 라우트 파일에서는 프로젝트 토큰 기반 클래스만 사용해야 하므로 토큰 클래스로 치환해 주세요.
As per coding guidelines
src/app/**/*.{tsx,jsx}: "Never use Tailwind base classes ... Always use project custom tokens fromglobal.css."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/app/`(service)/(my)/my-inquiry/write/page.tsx around lines 157 - 168, The checkbox inputs (the input tied to notifyKakao and the earlier email-notify input) use a hardcoded Tailwind color class "accent-rose-500"; replace that with the project's token-based class from global.css (e.g., the project's checkbox accent token such as "accent-primary" or the established token class used elsewhere in the app) so the inputs use the design token instead of a Tailwind base class; update both input elements' className values to the token class and ensure the same token is used consistently for all checkboxes in this component.src/components/payment/modals/class-cancel-payment-modal.tsx (1)
28-35:⚠️ Potential issue | 🟠 Major | ⚡ Quick win요청 중 닫기와 성공 콜백 사이 상태 경합을 막아주세요.
요청이 pending인 동안 닫으면, 이후
onSuccess가 도착해step을complete로 다시 바꿀 수 있어 재오픈 시 완료 화면이 먼저 노출될 수 있습니다.수정 예시
+ import { useEffect, useRef, useState } from 'react'; ... const [step, setStep] = useState<'form' | 'complete'>('form'); + const isOpenRef = useRef(open); + useEffect(() => { + isOpenRef.current = open; + }, [open]); const handleConfirm = () => { cancelPayment( { courseId, paymentId }, { - onSuccess: () => setStep('complete'), + onSuccess: () => { + if (isOpenRef.current) setStep('complete'); + }, }, ); }; const handleClose = () => { + if (isPending) return; onOpenChange(false); setStep('form'); };Also applies to: 37-40
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/payment/modals/class-cancel-payment-modal.tsx` around lines 28 - 35, The confirm handler can race with modal close: when cancelPayment is pending and the modal is closed, its onSuccess can later call setStep('complete') causing the completed view to show on reopen; to fix, introduce a stable flag/ref (e.g., isOpenRef or isActiveRef) that is set true while the modal is mounted/open and set false in the modal close/unmount handler, then change the cancelPayment onSuccess callback in handleConfirm to check that flag before calling setStep('complete') (or use the mutation promise and check the flag before setStep); apply the same pattern to the other cancelPayment call referenced at the 37-40 block so callbacks are ignored when the modal is closed.
🧹 Nitpick comments (6)
src/app/(landing)/class/[slug]/(learning)/qa/write/page.tsx (1)
95-105: 💤 Low value
handleSaveDraft에서 빈 내용 검증이 누락되었습니다.
handleSubmit에서는text내용이 비어있는지 검증하지만(Line 112-115),handleSaveDraft는 레슨 선택만 검증합니다. 사용자가 내용 없이 임시저장할 수 있으며, 이는 의도된 동작일 수도 있지만 피드 작성 페이지의handleSaveDraft와 일관성이 없습니다.💡 피드 페이지와 일관된 검증 추가 제안
function handleSaveDraft() { if (!selectedLessonId) { showToast('레슨을 선택해주세요.', 'error'); return; } + if (!text.replace(/<[^>]*>/g, '').trim()) { + showToast('내용을 입력해주세요.', 'error'); + return; + } localStorage.setItem( `course-qna-draft-${slug}`, JSON.stringify({ content: text, lessonId: selectedLessonId }), ); showToast('임시저장되었습니다.'); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/app/`(landing)/class/[slug]/(learning)/qa/write/page.tsx around lines 95 - 105, handleSaveDraft currently only checks selectedLessonId and allows saving when text is empty; update handleSaveDraft to validate that text is non-empty (same as handleSubmit) before calling localStorage.setItem: if text is empty, call showToast with the same empty-content error message and return. Keep the existing localStorage key (`course-qna-draft-${slug}`) and payload shape ({ content: text, lessonId: selectedLessonId }) and ensure you reference the same variables (handleSaveDraft, selectedLessonId, text, slug) so behavior matches handleSubmit.src/app/(service)/(my)/my-class/page.tsx (3)
44-48: 💤 Low valuelocalStorage 초기 로딩 시 깜빡임 개선 고려
현재
locallyDisabled상태가false로 시작한 후useEffect에서localStorage를 읽어 업데이트하므로, 실제로 비활성화된 상태였다면 초기 렌더링 시 잠깐 활성화된 UI가 보이다가 곧바로 비활성 UI로 전환되는 깜빡임(FOUC)이 발생할 수 있습니다.사용자 경험 개선을 위해 mounting 상태를 추적하여 첫 로드 시 로딩 상태를 표시하거나, 또는
useState의 lazy initialization을 활용하는 방법을 고려할 수 있습니다. (단, SSR 환경에서는 여전히 제약이 있으므로 mounting 플래그가 더 안전합니다.)개선 예시
const [locallyDisabled, setLocallyDisabled] = useState(false); + const [mounted, setMounted] = useState(false); useEffect(() => { setLocallyDisabled( localStorage.getItem(NOTIFICATION_DISABLE_KEY) === 'true', ); + setMounted(true); }, []); + if (!mounted) { + return <div>로딩 중...</div>; // 또는 스켈레톤 UI + }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/app/`(service)/(my)/my-class/page.tsx around lines 44 - 48, The initial render causes a flash because locallyDisabled is initialized to false then updated inside useEffect; change initialization to read localStorage lazily (e.g., useState(() => typeof window !== 'undefined' ? localStorage.getItem(NOTIFICATION_DISABLE_KEY) === 'true' : false)) or, safer for SSR, add a mounting flag (isMounted state set to true in useEffect) and render a loading/placeholder until isMounted is true before using locallyDisabled; update references to useEffect, setLocallyDisabled, locallyDisabled and NOTIFICATION_DISABLE_KEY accordingly so the UI doesn't render the wrong state on first paint.
74-86: 💤 Low valueToggleSwitch className에
cn()사용 권장프로젝트 가이드라인에 따르면 모든 className 조합은
cn()함수를 사용하도록 되어 있습니다. 현재 코드는 정상 동작하지만, 일관성을 위해cn()을 사용하는 것을 권장합니다.♻️ 개선 제안
<ToggleSwitch.Root checked={isEnabled} onCheckedChange={(checked) => { if (checked) { setAlarmModalOpen(true); } else { setDisableModalOpen(true); } }} size="lg" - className="bg-border-subtle data-[state=checked]:bg-fill-brand-default-default" + className={cn( + "bg-border-subtle", + "data-[state=checked]:bg-fill-brand-default-default" + )} />As per coding guidelines: "Always use
cn()function forclassNamecomposition in React components. Do not use template literal classNames."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/app/`(service)/(my)/my-class/page.tsx around lines 74 - 86, The ToggleSwitch.Root component is using a raw className string instead of the project's className composer; update the className prop on ToggleSwitch.Root to call the shared cn() helper (e.g., cn("bg-border-subtle", "data-[state=checked]:bg-fill-brand-default-default")) so all class composition follows the guideline; locate the ToggleSwitch.Root usage in page.tsx and replace the literal className value with a cn(...) call while keeping the existing classes and other props (checked, onCheckedChange, size) unchanged.
27-64: localStorage 기반 알림 비활성화의 다중 기기 사용성 고려현재 알림 비활성화 상태를
localStorage에 저장하는 설계는 디바이스별로 독립적으로 관리됩니다. 이는 다음과 같은 특성이 있습니다:
- 사용자가 여러 기기(PC, 모바일 등)를 사용하는 경우, 한 기기에서 끈 알림이 다른 기기에는 반영되지 않음
- 브라우저 캐시 삭제 시 설정이 초기화됨
- 서버 설정(
notificationSetting.isEnabled)과 클라이언트 설정이 분리되어 관리됨사용자가 "모든 기기에서 일괄적으로 알림을 끄고 싶다"는 요구가 있을 경우, 향후 서버 측에 임시 비활성화 플래그를 추가하는 것을 고려할 수 있습니다. 현재 설계가 의도된 것이라면 사용자에게 이 동작을 명확히 안내하는 것도 도움이 될 수 있습니다.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/app/`(service)/(my)/my-class/page.tsx around lines 27 - 64, The current per-device disable uses localStorage (NOTIFICATION_DISABLE_KEY) which won't sync across devices; either (A) persist the disable flag server-side by adding an API and updating useGetNotificationSetting to return a user-scoped disable flag and modify handleDisableConfirm and handleAlarmSuccess to call that API (instead of localStorage) so notificationSetting.isEnabled reflects the server state, or (B) keep localStorage but update the UI (near the disable modal and in MyClassPage where isEnabled is computed) to show a clear notice that "Disabling is device-specific and will not affect other devices or survive browser cache clears"; choose one and update the functions handleDisableConfirm, handleAlarmSuccess, and the useGetNotificationSetting integration accordingly.src/hooks/queries/course/course-api.ts (1)
972-976: ⚡ Quick win
status파라미터 타입을 리터럴 유니온으로 좁혀 주세요.현재
status?: string이라서 잘못된 상태값이 API 파라미터/쿼리키에 들어가도 컴파일 단계에서 걸러지지 않습니다.status?: 'DRAFT' | 'PUBLISHED'(또는 공용 타입)으로 제한하는 편이 안전합니다.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/hooks/queries/course/course-api.ts` around lines 972 - 976, The status parameter on useGetMyBuilderFeedManagement is typed too broadly as string; narrow it to a literal union (e.g., status?: 'DRAFT' | 'PUBLISHED') or reuse a shared enum/type if one exists (e.g., FeedStatus) to ensure invalid values are caught at compile time; update the function signature for useGetMyBuilderFeedManagement and any referenced types passed into the API/React Query key so callers and query keys use the new union/type consistently.src/app/(service)/(my)/my-posts/page.tsx (1)
107-110: ⚡ Quick win
src/app경로에서 Tailwind base/하드코딩 색상 클래스를 토큰 클래스로 치환해 주세요.
border-rose-500,text-gray-800,bg-white,shadow-md같은 클래스는 이 경로 규칙과 충돌합니다. 프로젝트 토큰(text-text-*,border-border-*,bg-fill-*,shadow-*)으로 통일하는 게 필요합니다.As per coding guidelines "
src/app/**/*.{tsx,jsx}: Never use Tailwind base classes ... Always use project custom tokens fromglobal.css".Also applies to: 126-127, 296-297, 313-314, 377-377, 396-396
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/app/`(service)/(my)/my-posts/page.tsx around lines 107 - 110, Replace Tailwind base color/utility classes in the page component className strings with the project's token classes: where you see `border-rose-500` use the border token family (`border-border-*`), `text-gray-800` -> `text-text-*`, `bg-white` -> `bg-fill-*`, and `shadow-md` -> `shadow-*`; update the conditional used around `activeTab === id` (the class string that chooses `'border-b-2 border-rose-500 text-rose-500'` vs `'font-designer-20r text-gray-800'`) and the other occurrences referenced (near the other className strings at the noted ranges) to use `border-border-...`, `text-text-...`, `bg-fill-...`, and `shadow-...` tokens respectively so all classes under src/app use the project tokens from global.css.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/ci.yml:
- Around line 178-189: The "Start local server" step currently backgrounds
"NEXT_PUBLIC_API_BASE_URL=... yarn start &" and polls localhost:3000 up to 30
times but does not fail the job if the server never becomes ready; update that
step to detect when the loop times out and explicitly fail (e.g., echo a clear
error like "Local server failed to start" and exit 1) so CI stops on server
startup failure. Target the polling block that uses curl --max-time 5 and the
for i in $(seq 1 30) loop and add a post-loop check that exits non-zero if the
server never became responsive.
In `@src/app/`(landing)/class/[slug]/(learning)/qa/write/page.tsx:
- Around line 265-271: The 임시저장 button is missing a disabled prop while a QnA
create request may be pending; update the button element that calls
handleSaveDraft to include disabled={createQna.isPending} (or
disabled={createQna.isPending || updateQna?.isPending} if an update flow exists)
so it mirrors the feed page behavior and prevents clicks during in-flight
requests.
In `@src/app/`(service)/(my)/class-payment-management/page.tsx:
- Line 265: The thumbnail div in page.tsx uses hardcoded Tailwind color classes
("from-rose-500 to-rose-300") which violates the project token rule; update the
JSX element (the div with className "h-1000 w-1000 flex-shrink-0 rounded-150
bg-gradient-to-br from-rose-500 to-rose-300") to replace those base color
classes with the corresponding project custom token classes defined in
global.css (use the token names your project provides for the gradient start/end
colors so the class becomes bg-gradient-to-br from-<token-name>
to-<token-name>), ensuring you only swap the color classes and keep the
size/shape classes unchanged.
In `@src/app/`(service)/(my)/my-inquiry/page.tsx:
- Around line 18-21: The ANSWER_COMPLETED status object currently hardcodes
Tailwind base classes in its className ('bg-rose-50 text-rose-500 border
border-rose-200'); replace those base classes with the corresponding project
token classes defined in global.css (use the project token for background, text
color and border color) by updating ANSWER_COMPLETED.className so it references
the custom token utility classes instead of 'text-rose-500' and
'border-rose-200'.
In `@src/app/`(service)/(my)/my-inquiry/write/page.tsx:
- Around line 52-54: handleDraft currently calls saveDraft({ id: 0, ... }) which
hardcodes id=0 and can be misinterpreted by the backend; change the flow in
handleDraft/saveDraft so you first call the appropriate "create draft" endpoint
when no oneToOneInquiryId exists (or verify backend contract) and then use the
returned oneToOneInquiryId for subsequent "patch draft" calls instead of always
PATCHing id: 0 (reference saveDraft and handleDraft to implement
create-then-update flow and consume the returned oneToOneInquiryId). Also
replace the hardcoded checkbox className accent-rose-500 with the project-wide
accent token defined in global.css (update the checkbox component/class usage to
use the token name instead of the explicit color token).
In `@src/app/`(service)/(my)/my-page/page.tsx:
- Around line 369-373: The JSX uses forbidden Tailwind base/arbitrary classes:
remove the bg-white in the outer element's className and replace it with the
repo's token-based background utility (e.g., the project background token like
bg-[token-name] or the designated bg utility), and replace the inner div's
arbitrary inset-[12.5%] with a predefined spacing/position token or a global.css
utility (update the class on the <div className="absolute inset-[12.5%]"> and
the outer className that contains bg-white). Ensure you use the repo's
predefined spacing/position tokens and background token utilities consistent
with other src/app/**/*.tsx files.
In `@src/app/`(service)/(my)/my-posts/page.tsx:
- Around line 457-461: The editHref construction uses feed.courseId as a slug
placeholder which can break route contracts; update the data flow to use a real
courseSlug instead: extend the API response/type and the hook that fetches feeds
to include courseSlug (or perform an id→slug lookup/mapping before rendering),
then change editHref (where editHref and feed.courseId/ feed.lessonId/
feed.feedId are referenced) to build URLs with feed.courseSlug (and preserve
lessonId/feedId query) so routes match the [slug] contract.
In `@src/components/payment/modals/class-refund-request-modal.tsx`:
- Around line 44-46: The handleConfirm path uses a bare "as" cast for reason and
silently returns if missing; replace the cast with a runtime guard using the
"in" operator (or explicit typeof/check) to validate required fields before
calling requestRefund in handleConfirm, and when reason is missing show a
user-facing toast error (e.g., via your existing toast API) instead of a silent
return; apply the same pattern to the similar logic around the code referenced
at 132-136 so all optional fields are runtime-validated and produce an error
toast when required data is unavailable.
- Around line 44-57: The onSuccess callback of requestRefund (used in
handleConfirm and the similar block around lines 60-73) unconditionally calls
setStep('complete'), which can overwrite the step after the modal has been
closed and reopened; add a modal-closed guard using a ref (e.g., isClosedRef or
isMountedRef) and set that ref when the modal closes, then in the onSuccess (and
any other request callbacks) check the ref before calling setStep('complete')
and also prevent closing the modal while the request is pending by tracking a
pending flag (e.g., isPendingRef or state) so close handlers and UI close
controls are disabled until request finishes.
---
Outside diff comments:
In `@src/app/`(service)/(my)/my-inquiry/write/page.tsx:
- Around line 157-168: The checkbox inputs (the input tied to notifyKakao and
the earlier email-notify input) use a hardcoded Tailwind color class
"accent-rose-500"; replace that with the project's token-based class from
global.css (e.g., the project's checkbox accent token such as "accent-primary"
or the established token class used elsewhere in the app) so the inputs use the
design token instead of a Tailwind base class; update both input elements'
className values to the token class and ensure the same token is used
consistently for all checkboxes in this component.
In `@src/components/payment/modals/class-cancel-payment-modal.tsx`:
- Around line 28-35: The confirm handler can race with modal close: when
cancelPayment is pending and the modal is closed, its onSuccess can later call
setStep('complete') causing the completed view to show on reopen; to fix,
introduce a stable flag/ref (e.g., isOpenRef or isActiveRef) that is set true
while the modal is mounted/open and set false in the modal close/unmount
handler, then change the cancelPayment onSuccess callback in handleConfirm to
check that flag before calling setStep('complete') (or use the mutation promise
and check the flag before setStep); apply the same pattern to the other
cancelPayment call referenced at the 37-40 block so callbacks are ignored when
the modal is closed.
---
Nitpick comments:
In `@src/app/`(landing)/class/[slug]/(learning)/qa/write/page.tsx:
- Around line 95-105: handleSaveDraft currently only checks selectedLessonId and
allows saving when text is empty; update handleSaveDraft to validate that text
is non-empty (same as handleSubmit) before calling localStorage.setItem: if text
is empty, call showToast with the same empty-content error message and return.
Keep the existing localStorage key (`course-qna-draft-${slug}`) and payload
shape ({ content: text, lessonId: selectedLessonId }) and ensure you reference
the same variables (handleSaveDraft, selectedLessonId, text, slug) so behavior
matches handleSubmit.
In `@src/app/`(service)/(my)/my-class/page.tsx:
- Around line 44-48: The initial render causes a flash because locallyDisabled
is initialized to false then updated inside useEffect; change initialization to
read localStorage lazily (e.g., useState(() => typeof window !== 'undefined' ?
localStorage.getItem(NOTIFICATION_DISABLE_KEY) === 'true' : false)) or, safer
for SSR, add a mounting flag (isMounted state set to true in useEffect) and
render a loading/placeholder until isMounted is true before using
locallyDisabled; update references to useEffect, setLocallyDisabled,
locallyDisabled and NOTIFICATION_DISABLE_KEY accordingly so the UI doesn't
render the wrong state on first paint.
- Around line 74-86: The ToggleSwitch.Root component is using a raw className
string instead of the project's className composer; update the className prop on
ToggleSwitch.Root to call the shared cn() helper (e.g., cn("bg-border-subtle",
"data-[state=checked]:bg-fill-brand-default-default")) so all class composition
follows the guideline; locate the ToggleSwitch.Root usage in page.tsx and
replace the literal className value with a cn(...) call while keeping the
existing classes and other props (checked, onCheckedChange, size) unchanged.
- Around line 27-64: The current per-device disable uses localStorage
(NOTIFICATION_DISABLE_KEY) which won't sync across devices; either (A) persist
the disable flag server-side by adding an API and updating
useGetNotificationSetting to return a user-scoped disable flag and modify
handleDisableConfirm and handleAlarmSuccess to call that API (instead of
localStorage) so notificationSetting.isEnabled reflects the server state, or (B)
keep localStorage but update the UI (near the disable modal and in MyClassPage
where isEnabled is computed) to show a clear notice that "Disabling is
device-specific and will not affect other devices or survive browser cache
clears"; choose one and update the functions handleDisableConfirm,
handleAlarmSuccess, and the useGetNotificationSetting integration accordingly.
In `@src/app/`(service)/(my)/my-posts/page.tsx:
- Around line 107-110: Replace Tailwind base color/utility classes in the page
component className strings with the project's token classes: where you see
`border-rose-500` use the border token family (`border-border-*`),
`text-gray-800` -> `text-text-*`, `bg-white` -> `bg-fill-*`, and `shadow-md` ->
`shadow-*`; update the conditional used around `activeTab === id` (the class
string that chooses `'border-b-2 border-rose-500 text-rose-500'` vs
`'font-designer-20r text-gray-800'`) and the other occurrences referenced (near
the other className strings at the noted ranges) to use `border-border-...`,
`text-text-...`, `bg-fill-...`, and `shadow-...` tokens respectively so all
classes under src/app use the project tokens from global.css.
In `@src/hooks/queries/course/course-api.ts`:
- Around line 972-976: The status parameter on useGetMyBuilderFeedManagement is
typed too broadly as string; narrow it to a literal union (e.g., status?:
'DRAFT' | 'PUBLISHED') or reuse a shared enum/type if one exists (e.g.,
FeedStatus) to ensure invalid values are caught at compile time; update the
function signature for useGetMyBuilderFeedManagement and any referenced types
passed into the API/React Query key so callers and query keys use the new
union/type consistently.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 98c6ea1a-3567-40f6-82f8-5584261b8482
⛔ Files ignored due to path filters (2)
public/my-page/discord-icon.pngis excluded by!**/*.pngpublic/my-page/feed-icon.svgis excluded by!**/*.svg
📒 Files selected for processing (25)
.github/workflows/ci.ymle2e/class/builder-feed.spec.tssrc/api/client/axios.tssrc/app/(class-lesson)/class/[slug]/lesson/[id]/_components/lesson-qna-submission-modal.tsxsrc/app/(landing)/class/[slug]/(learning)/feed/write/page.tsxsrc/app/(landing)/class/[slug]/(learning)/qa/write/page.tsxsrc/app/(service)/(my)/class-payment-management/page.tsxsrc/app/(service)/(my)/my-class/_components/disable-notification-modal.tsxsrc/app/(service)/(my)/my-class/_components/learning-notification-modal.tsxsrc/app/(service)/(my)/my-class/page.tsxsrc/app/(service)/(my)/my-inquiry/page.tsxsrc/app/(service)/(my)/my-inquiry/write/page.tsxsrc/app/(service)/(my)/my-page/_components/withdrawal-confirm-modal.tsxsrc/app/(service)/(my)/my-page/page.tsxsrc/app/(service)/(my)/my-posts/page.tsxsrc/app/(service)/(my)/payment-management/page.tsxsrc/components/common/layout/sidebar/my-page-mobile-nav.tsxsrc/components/common/layout/sidebar/my-page-sidebar.tsxsrc/components/common/modals/user-profile-modal.tsxsrc/components/payment/modals/class-cancel-payment-modal.tsxsrc/components/payment/modals/class-refund-request-modal.tsxsrc/hooks/queries/course/course-api.tssrc/hooks/queries/my-inquiry/inquiry-api.tssrc/types/api/course.types.tssrc/types/schemas/inquiry.schema.ts
| <div className="border-border-subtle rounded-200 flex gap-300 border p-300"> | ||
| {/* 썸네일 */} | ||
| <div className="h-1000 w-1000 flex-shrink-0 rounded-150 bg-gradient-to-br from-primary-500 to-rose-300" /> | ||
| <div className="h-1000 w-1000 flex-shrink-0 rounded-150 bg-gradient-to-br from-rose-500 to-rose-300" /> |
There was a problem hiding this comment.
썸네일 배경 그라디언트에 하드코딩 Tailwind 색상을 사용하고 있습니다.
from-rose-500 to-rose-300는 토큰 기반 클래스 규칙에 맞지 않아 프로젝트 토큰으로 교체가 필요합니다.
As per coding guidelines src/app/**/*.{tsx,jsx}: "Never use Tailwind base classes ... Always use project custom tokens from global.css."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/app/`(service)/(my)/class-payment-management/page.tsx at line 265, The
thumbnail div in page.tsx uses hardcoded Tailwind color classes ("from-rose-500
to-rose-300") which violates the project token rule; update the JSX element (the
div with className "h-1000 w-1000 flex-shrink-0 rounded-150 bg-gradient-to-br
from-rose-500 to-rose-300") to replace those base color classes with the
corresponding project custom token classes defined in global.css (use the token
names your project provides for the gradient start/end colors so the class
becomes bg-gradient-to-br from-<token-name> to-<token-name>), ensuring you only
swap the color classes and keep the size/shape classes unchanged.
| ANSWER_COMPLETED: { | ||
| label: '답변 완료', | ||
| className: 'bg-rose-50 text-rose-500 border border-rose-200', | ||
| }, |
There was a problem hiding this comment.
상태 배지에 하드코딩 색상 클래스가 포함되어 있습니다.
text-rose-500, border-rose-200 대신 global.css의 프로젝트 토큰 클래스로 교체해 주세요.
As per coding guidelines src/app/**/*.{tsx,jsx}: "Never use Tailwind base classes ... Always use project custom tokens from global.css."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/app/`(service)/(my)/my-inquiry/page.tsx around lines 18 - 21, The
ANSWER_COMPLETED status object currently hardcodes Tailwind base classes in its
className ('bg-rose-50 text-rose-500 border border-rose-200'); replace those
base classes with the corresponding project token classes defined in global.css
(use the project token for background, text color and border color) by updating
ANSWER_COMPLETED.className so it references the custom token utility classes
instead of 'text-rose-500' and 'border-rose-200'.
| await saveDraft({ | ||
| id: 0, | ||
| request: { |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# 목적: draft API 경로 계약과 호출부가 일치하는지 확인
# 기대: {id}/draft가 "기존 초안 ID"를 요구하는 계약인지 확인되면 현재 id=0 고정은 수정 필요
rg -n -C3 "one-to-one-inquiries/.*/draft|one-to-one-inquiries/\\$\\{id\\}/draft|saveDraft\\("
# 호출부에서 id 전달 방식 확인
rg -n -C3 "useSaveDraftOneToOneInquiry|mutateAsync: saveDraft|id:\\s*0"Repository: code-zero-to-one/study-platform-client
Length of output: 3423
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="src/app/(service)/(my)/my-inquiry/write/page.tsx"
API="src/hooks/queries/my-inquiry/inquiry-api.ts"
echo "---- page.tsx (lines 1-120) ----"
nl -ba "$FILE" | sed -n '1,120p'
echo "---- page.tsx (lines 120-220) ----"
nl -ba "$FILE" | sed -n '120,220p'
echo "---- inquiry-api.ts (lines 70-150) ----"
nl -ba "$API" | sed -n '70,160p'Repository: code-zero-to-one/study-platform-client
Length of output: 176
🏁 Script executed:
#!/bin/bash
set -euo pipefail
PAGE="src/app/(service)/(my)/my-inquiry/write/page.tsx"
API="src/hooks/queries/my-inquiry/inquiry-api.ts"
echo "== rg accent-rose-500 in page.tsx =="
rg -n --fixed-string "accent-rose-500" "$PAGE" || true
echo "== rg saveDraftOneToOneInquiry id usage =="
rg -n "useSaveDraftOneToOneInquiry|mypage/one-to-one-inquiries/\\$\\{id\\}/draft|saveDraft\\(" "$API" "$PAGE"
echo "== show page.tsx around saveDraft call =="
START=35; END=90
sed -n "${START},${END}p" "$PAGE" | cat
echo "== show page.tsx around first accent occurrence (±40 lines if found) =="
if rg -n --fixed-string "accent-rose-500" "$PAGE" >/tmp/acc_lines.txt 2>/dev/null; then
LINE=$(head -n1 /tmp/acc_lines.txt | cut -d: -f1)
START=$((LINE-40)); if [ $START -lt 1 ]; then START=1; fi
END=$((LINE+40))
sed -n "${START},${END}p" "$PAGE" | cat
fi
echo "== show inquiry-api.ts around saveDraft hook =="
# locate line range dynamically
SAVE_LINE=$(rg -n "useSaveDraftOneToOneInquiry" "$API" | head -n1 | cut -d: -f1)
START=$((SAVE_LINE-20)); if [ $START -lt 1 ]; then START=1; fi
END=$((SAVE_LINE+80))
sed -n "${START},${END}p" "$API" | catRepository: code-zero-to-one/study-platform-client
Length of output: 3449
🏁 Script executed:
#!/bin/bash
set -euo pipefail
PAGE="src/app/(service)/(my)/my-inquiry/write/page.tsx"
API="src/hooks/queries/my-inquiry/inquiry-api.ts"
echo "== rg accent-rose-500 in page.tsx =="
rg -n --fixed-string "accent-rose-500" "$PAGE" || true
echo "== rg saveDraftOneToOneInquiry id usage =="
rg -n "useSaveDraftOneToOneInquiry|mypage/one-to-one-inquiries/\\$\\{id\\}/draft|saveDraft\\(" "$API" "$PAGE"
echo "== show page.tsx around saveDraft call =="
START=35; END=90
sed -n "${START},${END}p" "$PAGE" | cat
echo "== show page.tsx around first accent occurrence (±40 lines if found) =="
if rg -n --fixed-string "accent-rose-500" "$PAGE" >/tmp/acc_lines.txt 2>/dev/null; then
LINE=$(head -n1 /tmp/acc_lines.txt | cut -d: -f1)
START=$((LINE-40)); if [ $START -lt 1 ]; then START=1; fi
END=$((LINE+40))
sed -n "${START},${END}p" "$PAGE" | cat
fi
echo "== show inquiry-api.ts around saveDraft hook =="
SAVE_LINE=$(rg -n "useSaveDraftOneToOneInquiry" "$API" | head -n1 | cut -d: -f1)
START=$((SAVE_LINE-20)); if [ $START -lt 1 ]; then START=1; fi
END=$((SAVE_LINE+80))
sed -n "${START},${END}p" "$API" | catRepository: code-zero-to-one/study-platform-client
Length of output: 3449
🏁 Script executed:
#!/bin/bash
set -euo pipefail
PAGE="src/app/(service)/(my)/my-inquiry/write/page.tsx"
API="src/hooks/queries/my-inquiry/inquiry-api.ts"
echo "== page.tsx: hasDraft/params/기존 inquiryId 추출부 확인 (1-120) =="
sed -n '1,140p' "$PAGE" | cat
echo "== page.tsx: draft 관련 상태/props/데이터 매핑 확인 (140-240) =="
sed -n '140,240p' "$PAGE" | cat
echo "== inquiry-api.ts: DraftOneToOneInquiryRequest 타입/주변(1-120) =="
sed -n '1,140p' "$API" | cat
echo "== inquiry-api.ts: CreateOneToOneInquiryRequest 및 기타 드래프트 관련 타입(140-240) =="
sed -n '140,260p' "$API" | cat
echo "== page.tsx: accent- 관련 토큰 혹시 다른 형태 존재하는지(rose/gray/red 등) =="
rg -n "accent-|rose|rose-" "$PAGE" || trueRepository: code-zero-to-one/study-platform-client
Length of output: 11018
🏁 Script executed:
#!/bin/bash
set -euo pipefail
PAGE="src/app/(service)/(my)/my-inquiry/write/page.tsx"
API="src/hooks/queries/my-inquiry/inquiry-api.ts"
echo "== page.tsx: hasDraft/params/기존 inquiryId 추출부 확인 (1-140) =="
sed -n '1,140p' "$PAGE" | cat
echo "== page.tsx: draft 관련 상태/props/데이터 매핑 확인 (140-240) =="
sed -n '140,240p' "$PAGE" | cat
echo "== inquiry-api.ts: DraftOneToOneInquiryRequest 타입/주변(1-140) =="
sed -n '1,140p' "$API" | cat
echo "== inquiry-api.ts: CreateOneToOneInquiryRequest 및 기타 드래프트 관련 타입(140-260) =="
sed -n '140,260p' "$API" | cat
echo "== page.tsx: accent- 관련 토큰 혹시 다른 형태 존재하는지(rose/gray/red 등) =="
rg -n "accent-|rose|rose-" "$PAGE" || trueRepository: code-zero-to-one/study-platform-client
Length of output: 11018
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n "useSaveDraftOneToOneInquiry|one-to-one-inquiries/\\$\\{id\\}/draft|saveDraft\\(\\{" . \
--glob '!**/node_modules/**' --glob '!**/dist/**' || true
rg -n "id:\\s*0" src --glob '!**/node_modules/**' || trueRepository: code-zero-to-one/study-platform-client
Length of output: 613
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n "useSaveDraftOneToOneInquiry|one-to-one-inquiries/\\$\\{id\\}/draft|saveDraft\\(\\{" . \
--glob '!**/node_modules/**' --glob '!**/dist/**' || true
rg -n "id:\\s*0" src --glob '!**/node_modules/**' || trueRepository: code-zero-to-one/study-platform-client
Length of output: 613
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n "one-to-one-inquiries/.*/draft|/draft', request|/draft\`|/draft\)" src || true
rg -n "id:\s*0" src || true
rg -n "draft" src/hooks src/app || trueRepository: code-zero-to-one/study-platform-client
Length of output: 3933
임시저장 id: 0 고정과 체크박스 accent-rose-500 사용 수정 필요
handleDraft에서saveDraft({ id: 0, ... })로mypage/one-to-one-inquiries/${id}/draftPATCH를 호출 중인데, 서버가 이id를 기존oneToOneInquiryId로 해석한다면 초안 대상 식별 실패(404/검증 오류) 가능 → 백엔드에서id=0을 신규 draft 생성으로 처리하는지 계약 확인 후, 필요 시 초안 생성 → 반환된oneToOneInquiryId로 “초안 수정” 흐름으로 분리.- 체크박스에
className="... accent-rose-500"가 실제로 사용 중이며, 하드코딩된 색상 토큰이라global.css의 프로젝트 accent 토큰으로 교체 필요.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/app/`(service)/(my)/my-inquiry/write/page.tsx around lines 52 - 54,
handleDraft currently calls saveDraft({ id: 0, ... }) which hardcodes id=0 and
can be misinterpreted by the backend; change the flow in handleDraft/saveDraft
so you first call the appropriate "create draft" endpoint when no
oneToOneInquiryId exists (or verify backend contract) and then use the returned
oneToOneInquiryId for subsequent "patch draft" calls instead of always PATCHing
id: 0 (reference saveDraft and handleDraft to implement create-then-update flow
and consume the returned oneToOneInquiryId). Also replace the hardcoded checkbox
className accent-rose-500 with the project-wide accent token defined in
global.css (update the checkbox component/class usage to use the token name
instead of the explicit color token).
| className="border-border-brand rounded-200 flex flex-1 items-center justify-center gap-175 overflow-hidden border bg-white py-250 transition-colors hover:bg-gray-50" | ||
| > | ||
| <div className="bg-primary-100 flex size-1000 shrink-0 items-center justify-center rounded-full"> | ||
| <span className="font-designer-16b text-primary-600">B</span> | ||
| <div className="relative h-675 w-675 shrink-0 overflow-hidden"> | ||
| <div className="absolute inset-[12.5%]"> | ||
| <Image |
There was a problem hiding this comment.
앱 라우트 스타일 가이드 위반 클래스 제거 필요
bg-white(베이스 클래스)와 inset-[12.5%](arbitrary value)는 이 리포의 src/app/**/*.tsx 규칙에 맞지 않습니다. 토큰 기반 배경 클래스와 사전정의된 spacing/position 토큰(또는 global.css 유틸)으로 교체해 주세요.
As per coding guidelines, src/app/**/*.{tsx,jsx}에서는 Tailwind base class와 arbitrary value 사용이 금지되어 있습니다.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/app/`(service)/(my)/my-page/page.tsx around lines 369 - 373, The JSX uses
forbidden Tailwind base/arbitrary classes: remove the bg-white in the outer
element's className and replace it with the repo's token-based background
utility (e.g., the project background token like bg-[token-name] or the
designated bg utility), and replace the inner div's arbitrary inset-[12.5%] with
a predefined spacing/position token or a global.css utility (update the class on
the <div className="absolute inset-[12.5%]"> and the outer className that
contains bg-white). Ensure you use the repo's predefined spacing/position tokens
and background token utilities consistent with other src/app/**/*.tsx files.
| // TODO: courseSlug not in API response — using courseId as slug placeholder | ||
| const editHref = | ||
| feed.lessonId !== null | ||
| ? `/class/${feed.courseId}/lesson/${feed.lessonId}?feedId=${feed.feedId}` | ||
| : `/class/${feed.courseId}`; |
There was a problem hiding this comment.
임시저장 수정 링크가 라우트 계약(slug)과 불일치할 가능성이 큽니다.
[slug] 자리에 courseId를 placeholder로 쓰고 있어 수정 페이지 이동이 깨질 수 있습니다. courseSlug를 API 응답/타입에 포함하거나, id→slug 매핑을 선행한 뒤 링크를 생성해야 합니다.
원하시면 courseSlug 계약 반영(타입/훅/링크 생성부) 기준으로 후속 이슈 템플릿을 바로 정리해드릴게요.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/app/`(service)/(my)/my-posts/page.tsx around lines 457 - 461, The
editHref construction uses feed.courseId as a slug placeholder which can break
route contracts; update the data flow to use a real courseSlug instead: extend
the API response/type and the hook that fetches feeds to include courseSlug (or
perform an id→slug lookup/mapping before rendering), then change editHref (where
editHref and feed.courseId/ feed.lessonId/ feed.feedId are referenced) to build
URLs with feed.courseSlug (and preserve lessonId/feedId query) so routes match
the [slug] contract.
| const handleConfirm = () => { | ||
| if (!reason) return; | ||
| requestRefund( |
There was a problem hiding this comment.
환불 사유는 런타임 가드로 검증하고, 누락 시 토스트를 보여주세요.
현재는 bare as 단언으로 reason을 설정하고, reason이 없으면 조용히 return 합니다. 잘못된 값 유입/무응답 UX를 함께 만들 수 있습니다.
수정 예시
+ import { useToastStore } from '`@/stores/use-toast-store`';
...
const [detail, setDetail] = useState('');
+ const showToast = useToastStore((state) => state.showToast);
const handleConfirm = () => {
- if (!reason) return;
+ if (!reason) {
+ showToast('환불 사유를 선택해 주세요.', 'error');
+ return;
+ }
requestRefund(
...
<SingleDropdown
options={REFUND_REASONS}
value={reason}
- onChange={(v) => setReason(v as CourseRefundReasonCode)}
+ onChange={(v) => {
+ const nextReason =
+ typeof v === 'string' &&
+ REFUND_REASONS.some((r) => r.value === v)
+ ? (v as CourseRefundReasonCode)
+ : undefined;
+ if (!nextReason) {
+ showToast('유효하지 않은 환불 사유예요.', 'error');
+ return;
+ }
+ setReason(nextReason);
+ }}
placeholder="사유를 선택해 주세요."
size="l"
/>As per coding guidelines "src/components/**/*.{ts,tsx}: Use the in guard operator with fallback ... Never use bare as assertions..." and "Guard handlers for optional fields ... show a toast error message if required data is unavailable".
Also applies to: 132-136
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/payment/modals/class-refund-request-modal.tsx` around lines 44
- 46, The handleConfirm path uses a bare "as" cast for reason and silently
returns if missing; replace the cast with a runtime guard using the "in"
operator (or explicit typeof/check) to validate required fields before calling
requestRefund in handleConfirm, and when reason is missing show a user-facing
toast error (e.g., via your existing toast API) instead of a silent return;
apply the same pattern to the similar logic around the code referenced at
132-136 so all optional fields are runtime-validated and produce an error toast
when required data is unavailable.
…틸 export Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ent HTML 스트립 공용화 - qa/write: 임시저장 버튼 createQna.isPending 중 disabled 처리 - class-refund: closedRef로 모달 닫힘 후 onSuccess step 덮어씀 방지 - my-posts: feedContent.replace() → stripHtml() 공용 유틸로 교체 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
♻️ Duplicate comments (2)
src/components/payment/modals/class-refund-request-modal.tsx (2)
49-51:⚠️ Potential issue | 🟠 Major | ⚡ Quick win환불 사유 검증이 여전히 런타임 가드 없이 처리되고, 누락 시 무응답입니다.
reason누락 시 조용히 반환하고(Line 50), 드롭다운 값도 bareas로 단언하고 있어 잘못된 값 유입 시 UX/안정성이 떨어집니다. 가드 + 토스트 처리로 바꿔 주세요.수정 예시
import { useRequestCourseRefund } from '`@/hooks/queries/course/course-api`'; +import { useToastStore } from '`@/stores/use-toast-store`'; import type { CourseRefundReasonCode } from '`@/types/api/course.types`'; +const REFUND_REASON_GUARD: Record<CourseRefundReasonCode, true> = { + REPAYMENT_AFTER_METHOD_CHANGE: true, + DUPLICATE_PAYMENT: true, + CHANGE_OF_MIND: true, + UNSATISFIED_CONTENT: true, + TECHNICAL_ISSUE: true, + OTHER: true, +}; + +const isCourseRefundReasonCode = ( + value: unknown, +): value is CourseRefundReasonCode => + typeof value === 'string' && value in REFUND_REASON_GUARD; + export default function ClassRefundRequestModal({ @@ }: ClassRefundRequestModalProps) { + const showToast = useToastStore((state) => state.showToast); @@ const handleConfirm = () => { - if (!reason) return; + if (!reason) { + showToast('환불 사유를 선택해 주세요.', 'error'); + return; + } @@ <SingleDropdown options={REFUND_REASONS} value={reason} - onChange={(v) => setReason(v as CourseRefundReasonCode)} + onChange={(v) => { + if (!isCourseRefundReasonCode(v)) { + showToast('유효하지 않은 환불 사유예요.', 'error'); + return; + } + setReason(v); + }} placeholder="사유를 선택해 주세요." size="l" />As per coding guidelines "
src/components/**/*.{ts,tsx}: Guard handlers for optional fields ... show a toast error message if required data is unavailable" and "Use theinguard operator with fallback ... Never use bareasassertions without runtime guards".Also applies to: 140-144
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/payment/modals/class-refund-request-modal.tsx` around lines 49 - 51, handleConfirm currently silently returns when reason is missing and uses bare "as" assertions for dropdown values; change it to runtime-guard the reason and dropdown value before calling requestRefund: inside handleConfirm check that reason is present using an "in" or truthy guard and if missing call the toast error helper (show a user-facing error) and return; for the dropdown value replace bare "as" assertions with a guarded type check (using "in" or a validated fallback) so you only pass a well-formed value to requestRefund; apply the same guard+toast pattern to the other handler around the 140-144 section.
45-47:⚠️ Potential issue | 🟠 Major | ⚡ Quick win닫았다가 재오픈하면 이전 요청의 성공 콜백이 새 모달 상태를 덮어쓸 수 있습니다.
closedRef를open=true에서 즉시false로 되돌려서, 이전 in-flight 요청이 늦게 성공하면 새 세션에서setStep('complete')가 다시 실행될 수 있습니다. 요청 단위 식별자(ref)를 같이 써서 stale 콜백을 무효화해 주세요.수정 예시
const { mutate: requestRefund, isPending } = useRequestCourseRefund(); const closedRef = useRef(false); + const requestSeqRef = useRef(0); useEffect(() => { - if (open) closedRef.current = false; + if (!open) { + closedRef.current = true; + requestSeqRef.current += 1; // close 시 이전 요청 콜백 무효화 + } }, [open]); const handleConfirm = () => { if (!reason) return; + const requestSeq = ++requestSeqRef.current; + closedRef.current = false; requestRefund( { paymentId, @@ { onSuccess: () => { - if (!closedRef.current) setStep('complete'); + if (closedRef.current || requestSeq !== requestSeqRef.current) return; + setStep('complete'); }, }, ); }; const handleClose = () => { closedRef.current = true; + requestSeqRef.current += 1; onOpenChange(false); setStep('form'); setReason(undefined); setDetail(''); }; const handleCompleteClose = () => { + closedRef.current = true; + requestSeqRef.current += 1; onOpenChange(false); setStep('form'); setReason(undefined); setDetail(''); router.push('/class-payment-management'); };Also applies to: 60-62, 68-73
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/payment/modals/class-refund-request-modal.tsx` around lines 45 - 47, The current useEffect resets closedRef on open which allows a previous in-flight request's success callback to affect a newly opened modal; add a per-request identifier ref (e.g., requestIdRef) that's incremented whenever the modal opens (inside the same useEffect or open handling), capture the current id when starting each async request, and in every success/finally callback (where setStep('complete') or other state updates occur) check that the captured id still matches requestIdRef.current before calling setStep or touching state; apply the same pattern to the other request sites mentioned (around the blocks corresponding to lines ~60-62 and ~68-73) so stale callbacks are ignored.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Duplicate comments:
In `@src/components/payment/modals/class-refund-request-modal.tsx`:
- Around line 49-51: handleConfirm currently silently returns when reason is
missing and uses bare "as" assertions for dropdown values; change it to
runtime-guard the reason and dropdown value before calling requestRefund: inside
handleConfirm check that reason is present using an "in" or truthy guard and if
missing call the toast error helper (show a user-facing error) and return; for
the dropdown value replace bare "as" assertions with a guarded type check (using
"in" or a validated fallback) so you only pass a well-formed value to
requestRefund; apply the same guard+toast pattern to the other handler around
the 140-144 section.
- Around line 45-47: The current useEffect resets closedRef on open which allows
a previous in-flight request's success callback to affect a newly opened modal;
add a per-request identifier ref (e.g., requestIdRef) that's incremented
whenever the modal opens (inside the same useEffect or open handling), capture
the current id when starting each async request, and in every success/finally
callback (where setStep('complete') or other state updates occur) check that the
captured id still matches requestIdRef.current before calling setStep or
touching state; apply the same pattern to the other request sites mentioned
(around the blocks corresponding to lines ~60-62 and ~68-73) so stale callbacks
are ignored.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: b79b6096-6e1c-42f4-ba94-b3012c2386ed
📒 Files selected for processing (5)
.github/workflows/ci.ymlsrc/app/(landing)/class/[slug]/(learning)/qa/write/page.tsxsrc/app/(service)/(my)/my-posts/page.tsxsrc/components/payment/modals/class-refund-request-modal.tsxsrc/utils/markdown-content-text.ts
개요
마이페이지 관련 미구현 기능(내가 작성한 글, 1:1 문의 페이지)을 신규 구현하고, 마이클래스 알림 토글 ON/OFF 양방향 제어를 추가했습니다. 탈퇴 모달 및 전역 토큰(
primary-500→ 시스템 토큰) 불일치를 Figma 기준으로 수정하고, 임시저장 draft 중복 생성 방지 및 DRAFT→PUBLISHED 전환 처리를 개선했습니다.원본 PR
fix/myPageCherry-pick 대상 커밋 (23개)
0faead4d— chore(api): v6 마이페이지 전용 axiosInstanceV6 추가1dab1a13— feat(my-inquiry): 1:1 문의 API 훅 및 Zod 스키마 추가bc196812— feat(my-page): 마이페이지 내가 작성한 글 / 1:1 문의 페이지 구현de057ddb— 마이 클래스 토글 비활성화 기능 추가 — localStorage 기반 클라이언트 상태 관리e756be3d— NotificationSettingResponse dead field 제거 (empty, skipped)c10631fb— 마이클래스 버튼 토큰 수정 및 코드 정리d4e14ded— feat: 마이페이지 배너 UI 업데이트 및 토큰 수정d88f208d— fix: 마이페이지 전역 primary-500 미존재 토큰 교체8aa20776— chore: 마이페이지 에셋 추가b0b76f53— fix: 탈퇴 확인 모달 Figma 디자인 반영546c3f05— chore(types): 클래스 결제 환불/취소 관련 타입 추가c558d1d0— chore(api): 클래스 결제 상세 조회 및 환불 요청 훅 추가f69a9abd— feat(결제관리): 클래스 결제 취소·환불 플로우 분리 및 가상계좌 상세 조회3f52f326— feat(마이피드): 임시 저장 피드 목록 UI 추가e8031bec— chore(types): 빌더 피드 관리 응답 타입 추가29cafd3a— fix: 문의 첨부 미연동 숨김, 알림 토글 hydration 및 에러 처리 개선33af944e— chore(types): MyDraftBuilderFeedItemResponse 미사용 타입 제거8d7bc206— refactor(api): useGetMyDraftBuilderFeeds 중복 훅 제거0db37fcc— fix(임시저장): draft 중복 생성 방지 및 DRAFT→PUBLISHED 전환 처리cca0ecfc— fix(피드 등록): 임시저장 발행 후 피드 상세 페이지로 이동f948eed1— 테스트 코드 수정3b836477— fix(ci): 스테이징 서버 다운 시 E2E 스킵으로 CI 보호06afccf0— fix(ci): non-@auth E2E를 로컬 서버 대상으로 실행해 스테이징 의존성 제거변경 파일
.github/workflows/ci.yml— E2E CI 개선 (스테이징 다운 시 스킵, 로컬 서버 대상 non-@auth 실행)e2e/class/builder-feed.spec.ts— 테스트 코드 수정 (networkidle → waitForResponse)public/my-page/discord-icon.png,feed-icon.svg— 마이페이지 에셋 추가src/api/client/axios.ts— axiosInstanceV6 추가src/app/(landing)/class/[slug]/(learning)/feed/write/page.tsx— 임시저장 draft 중복 방지, PUBLISHED 전환src/app/(landing)/class/[slug]/(learning)/qa/write/page.tsx— draft 정리 추가src/app/(service)/(my)/class-payment-management/page.tsx— 취소·환불 플로우 분리src/app/(service)/(my)/my-class/— 알림 토글 비활성화 모달 추가src/app/(service)/(my)/my-inquiry/— 1:1 문의 목록·작성 페이지 구현src/app/(service)/(my)/my-page/— 배너 UI, 탈퇴 모달 Figma 반영src/app/(service)/(my)/my-posts/page.tsx— 내가 작성한 글 + 임시저장 목록 구현src/components/payment/modals/class-refund-request-modal.tsx— 신규 환불 모달src/hooks/queries/course/course-api.ts— 결제 상세·환불·피드 관리 훅 추가src/hooks/queries/my-inquiry/inquiry-api.ts— 1:1 문의 API 훅 추가src/types/api/course.types.ts— 결제·피드 관련 타입 추가src/types/schemas/inquiry.schema.ts— 문의 Zod 스키마 추가혼입 검증 결과
fix/myPage브랜치에는 deploy-prod.yml, docs, 다른 E2E 파일 등 PR #690 범위 외 파일이 포함되어 있었으나, cherry-pick은 PR에 명시된 23개 커밋만 대상으로 진행했습니다.yarn typecheck— PR 변경 파일 타입 에러 0건yarn lint— 에러 0건 (기존 264 warnings은 PR 범위 외)Test plan
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Improvements